package com.zb.controller;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.sf.json.JSONArray;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.jdom.JDOMException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import com.wxinf.send.press.TokenUtil;
import com.zb.utils.JsonUtil;
import com.zb.utils.MD5Util;
import com.zb.utils.SettingsUtil;
import com.zb.utils.Sha1Util;
import com.zb.utils.TenpayHttpClient;
import com.zb.utils.TenpayUtil;
import com.zb.utils.WXUtil;
import com.zb.utils.XMLUtil;

/**
 * 微信支付-使用JSSDK发起支付
 * 
 * <p>简要说明：</p>
 * <p>JSSDK与使用浏览器自带对象发起支付，是不同的。总的来说，大体有以下几种：</p>
 * <p>1、JSSDK支付，需要在支付的页面引入jweixin-1.0.0.js，而使用微信浏览器自带对象发起支付不需要引入</p>
 * <p>2、JSSDK支付，后台需要接收当前支付页面的浏览器URL全路径，用于前端js中wx.config配置中的signature签名</p>
 * <p>3、JSSDK后台签名的次数达到3次，每次都是不同业务要求的签名，而使用微信浏览器自带的对象发起支付，只出现1次签名</p>
 * <p>4、JSSDK支付，除了wx.config配置中的签名使用SHA1加密之外，其他签名全部必须是MD5加密）</p>
 * <p>5、JSSDK支付，需要有ticket凭证才可调用js接口，获取ticket凭证需要token作为条件，所以还需要获取token，并缓存起来</p>
 * <p>6、除了controller方法中有部分区别之外，它们的签名都是一样的算法(JSSDK的wx.config配置中的signature签名算法除外)，都是MD5加密，调用同样的方法进行签名。</p>
 * <p>7、JSSDK支付，后台的每次签名，参与签名的参数都是不同的，一定要注意，否则会出现签名错误</p>
 * 
 * 作者: zhoubang 
 * 日期：2015年6月25日 下午6:25:05
 */
@Controller
@RequestMapping("chooseWXPay")
public class ChooseWXPayController {

    private static Logger log = LoggerFactory.getLogger(ChooseWXPayController.class);

    /** 支付密钥，商户平台 > API安全 > 密钥管理 中进行设置 */
    private static final String API_KEY = SettingsUtil.getInstance().getString("wx.apikey");

    /** 支付的回调方法，微信调用 */
    private static final String NOTIFY_URL = "chooseWXPay/pay";

    /** 获取预支付单号prepay_id */
    private static final String UNI_URL = SettingsUtil.getInstance().getString("wx.uniurl");

    /** 微信公众号APPID */
    private static final String APPID = SettingsUtil.getInstance().getString("wx.appid");

    /** 微信公众号绑定的商户号 */
    private static final String MCH_ID = SettingsUtil.getInstance().getString("wx.mchid");

    /** 测试微信号的openId，这里固定写成我的微信openid，你们到时候自己编码获取 */
    private static final String openId = "oPafystM-l_S2fYlSlK7buAxO9cM";

    /**
     * 生成订单数据以及微信支付需要的签名等信息，传输到前端，发起调用JSAPI支付
     * 
     * 作者: zhoubang 日期：2015年6月25日 上午10:39:49
     * 
     * @param request
     * @param commodityName
     *            商品名称
     * @param totalPrice
     *            支付总金额
     * @param clientUrl
     *            当前支付页面所处的浏览器url全路径，用于参加签名，该url只有JSSDK方式支付才使用到
     * @return
     * @throws Exception
     */
    @SuppressWarnings("unchecked")
    @RequestMapping(value = "gopay",method = RequestMethod.POST)
    public @ResponseBody String Gopay(HttpServletRequest request,
            String commodityName, double totalPrice, String clientUrl) throws Exception {
        log.info("clientUrl=" + clientUrl);
        
        String path = request.getContextPath();
        String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
        log.info("base path=" + basePath);

        SortedMap<Object, Object> parameters = new TreeMap<Object, Object>();
        SortedMap<Object, Object> configMap = new TreeMap<Object, Object>();
        parameters.put("appid", APPID);
        parameters.put("mch_id", MCH_ID);
        parameters.put("nonce_str", WXUtil.getNonceStr());
        parameters.put("body", commodityName);// 商品名称

        /** 当前时间 yyyyMMddHHmmss */
        String currTime = TenpayUtil.getCurrTime();
        /** 8位日期字符串 */
        String strTime = currTime.substring(8, currTime.length());
        /** 四位随机数 */
        String strRandom = TenpayUtil.buildRandom(4) + "";
        /** 订单号 */
        parameters.put("out_trade_no", strTime + strRandom);
        /** 订单金额以分为单位，只能为整数 */
        parameters.put("total_fee", "1");
        /** 客户端本地ip */
        parameters.put("spbill_create_ip", request.getRemoteAddr());
        /** 支付回调地址 */
        parameters.put("notify_url", basePath + NOTIFY_URL);
        /** 支付方式为JSAPI支付 */
        parameters.put("trade_type", "JSAPI");
        /** 用户微信的openid，当trade_type为JSAPI的时候，该属性字段必须设置 */
        parameters.put("openid", openId);
        
        /** 使用MD5进行签名，编码必须为UTF-8 */
        String sign = createSign_ChooseWXPay("UTF-8", parameters);
        
        /**将签名结果加入到map中，用于生成xml格式的字符串*/
        parameters.put("sign", sign);
        
        /** 生成xml结构的数据，用于统一下单请求的xml请求数据 */
        String requestXML = getRequestXml(parameters);
        log.info("requestXML：" + requestXML);
        
        /** 使用POST请求统一下单接口，获取预支付单号prepay_id */
        HttpClient client = new HttpClient();
        PostMethod myPost = new PostMethod(UNI_URL);
        client.getParams().setSoTimeout(300 * 1000);
        String result = null;
        try {
            myPost.setRequestEntity(new StringRequestEntity(requestXML, "text/xml", "utf-8"));
            int statusCode = client.executeMethod(myPost);
            if (statusCode == HttpStatus.SC_OK) {
                //使用流的方式解析微信服务器返回的xml结构的字符串
                BufferedInputStream bis = new BufferedInputStream(myPost.getResponseBodyAsStream());
                byte[] bytes = new byte[1024];
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                int count = 0;
                while ((count = bis.read(bytes)) != -1) {
                    bos.write(bytes, 0, count);
                }
                byte[] strByte = bos.toByteArray();
                result = new String(strByte, 0, strByte.length, "utf-8");
                bos.close();
                bis.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        /** 需要释放掉、关闭连接 */
        myPost.releaseConnection();
        client.getHttpConnectionManager().closeIdleConnections(0);

        log.info("请求统一支付返回:" + result);
        try {
            /** 解析微信返回的信息，以Map形式存储便于取值 */
            Map<String, String> map = XMLUtil.doXMLParse(result);
            log.info("预支付单号prepay_id为:" + map.get("prepay_id"));
            
            /**全局map，该map存放前端ajax请求的返回值信息，包括wx.config中的配置参数值，也包括wx.chooseWXPay中的配置参数值*/
            SortedMap<Object, Object> params = new TreeMap<Object, Object>();
            params.put("appId", APPID);
            params.put("timeStamp", new Date().getTime()); //时间戳
            params.put("nonceStr", WXUtil.getNonceStr()); //随机字符串
            params.put("package", "prepay_id=" + map.get("prepay_id")); //主意格式必须为 prepay_id=***
            params.put("signType", "MD5"); //签名的方式必须是MD5
            /**
             *获取预支付prepay_id后，需要再次签名，此次签名是用于前端js中的wx.chooseWXPay中的paySign。
             * 参与签名的参数有5个，分别是：appId、timeStamp、nonceStr、 package、signType 注意参数名称的大小写
             */
            String paySign = createSign_ChooseWXPay("UTF-8", params);
            /** 预支付单号 */
            params.put("packageValue", "prepay_id=" + map.get("prepay_id"));
            params.put("paySign", paySign); //支付签名
            /** 付款成功后同步请求的URL，请求我们自定义的支付成功的页面，展示给用户 */
            params.put("sendUrl", basePath + "chooseWXPay/paysuccess?totalPrice=1");
            /** 获取用户的微信客户端版本号，用于前端支付之前进行版本判断，微信版本低于5.0无法使用微信支付 */
            String userAgent = request.getHeader("user-agent");
            char agent = userAgent .charAt(userAgent.indexOf("MicroMessenger") + 15);
            params.put("agent", new String(new char[] { agent }));
            
            /**使用JSSDK支付，需要另一个凭证，也就是ticket。这个是JSSDK中使用到的。*/
            String jsapi_ticket = "";
            /**获取ticket，需要token作为参数传递,由于token有有效期限制，和调用次数限制，你可以缓存到session或者数据库中.有效期设置为小于7200秒*/
            String token = TokenUtil.getAccessToken().getAccessToken();
            log.info("获取的token值为:" + token);
            /**获取ticket的请求URL，需要token作为参数*/
            String getTicket = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=" + token;
            log.info("接口调用凭证ticket：" + getTicket);

            TenpayHttpClient httpClient = new TenpayHttpClient();
            httpClient.setMethod("GET");
            httpClient.setReqContent(getTicket);
            if (httpClient.call()) {
                log.info("获取ticket成功");
                String resContent = httpClient.getResContent();
                log.info("resContent：" + resContent);
                jsapi_ticket = JsonUtil.getJsonValue(resContent, "ticket");
                log.info("jsapi_ticket：" + jsapi_ticket);
            }
            // 获取到ticket凭证之后，需要进行一次签名
            String config_nonceStr = WXUtil.getNonceStr();// 获取随机字符串
            long config_timestamp = new Date().getTime();// 时间戳
            // 加入签名的参数有4个，分别是： noncestr、jsapi_ticket、timestamp、url，注意字母全部为小写
            configMap.put("noncestr", config_nonceStr);
            configMap.put("jsapi_ticket", jsapi_ticket);
            configMap.put("timestamp", config_timestamp);
            configMap.put("url", clientUrl);

            //该签名是用于前端js中wx.config配置中的signature值。
            String config_sign = createSign_wx_config("UTF-8", configMap);

            // 将config_nonceStr、jsapi_ticket 、config_timestamp、config_sign一同传递到前端
            // 这几个参数名称和上面获取预支付prepay_id使用的参数名称是不一样的，不要混淆了。
            // 这几个参数是提供给前端js代码在调用wx.config中进行配置的参数，wx.config里面的signature值就是这个config_sign的值，以此类推
            params.put("config_nonceStr", config_nonceStr);
            params.put("config_timestamp", config_timestamp);
            params.put("config_sign", config_sign);
            
            // 将map转换为json字符串，传递给前端ajax回调
            String json = JSONArray.fromObject(params).toString();
            log.info("用于wx.config配置的json：" + json);
            return json;

        } catch (JDOMException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }

    // SHA1加密，该加密是对wx.config配置中使用到的参数进行SHA1加密，这里不需要key参与加密
    public static String createSign_wx_config(String characterEncoding, SortedMap<Object, Object> parameters) {
        StringBuffer sb = new StringBuffer();
        Set<Entry<Object, Object>> es = parameters.entrySet();
        Iterator<Entry<Object, Object>> it = es.iterator();
        while (it.hasNext()) {
            Map.Entry<Object, Object> entry = (Map.Entry<Object, Object>) it.next();
            String k = (String) entry.getKey();
            Object v = entry.getValue();
            if (null != v && !"".equals(v)) {
                sb.append(k + "=" + v + "&");
            }
        }
        String str = sb.toString();
        String sign = Sha1Util.getSha1(str.substring(0, str.length() - 1));
        return sign;
    }
    /**
     * sign签名，必须使用MD5签名，且编码为UTF-8
     * 
     * 作者: zhoubang 日期：2015年6月10日 上午9:31:24
     * 
     * @param characterEncoding
     * @param parameters
     * @return
     */
    public static String createSign_ChooseWXPay(String characterEncoding, SortedMap<Object, Object> parameters) {
        StringBuffer sb = new StringBuffer();
        Set<Entry<Object, Object>> es = parameters.entrySet();
        Iterator<Entry<Object, Object>> it = es.iterator();
        while (it.hasNext()) {
            Map.Entry<Object, Object> entry = (Map.Entry<Object, Object>) it.next();
            String k = (String) entry.getKey();
            Object v = entry.getValue();
            if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {
                sb.append(k + "=" + v + "&");
            }
        }
        /** 支付密钥必须参与加密，放在字符串最后面 */
        sb.append("key=" + API_KEY);
        String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
        return sign;
    }

    /**
     * 将请求参数转换为xml格式的string字符串，微信服务器接收的是xml格式的字符串
     * 
     * 作者: zhoubang 日期：2015年6月10日 上午9:25:51
     * 
     * @param parameters
     * @return
     */
    public static String getRequestXml(SortedMap<Object, Object> parameters) {
        StringBuffer sb = new StringBuffer();
        sb.append("<xml>");
        Set<Entry<Object, Object>> es = parameters.entrySet();
        Iterator<Entry<Object, Object>> it = es.iterator();
        while (it.hasNext()) {
            Map.Entry<Object, Object> entry = (Map.Entry<Object, Object>) it.next();
            String k = (String) entry.getKey();
            String v = (String) entry.getValue();
            if ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k) || "sign".equalsIgnoreCase(k)) {
                sb.append("<" + k + ">" + "<![CDATA[" + v + "]]></" + k + ">");
            } else {
                sb.append("<" + k + ">" + v + "</" + k + ">");
            }
        }
        sb.append("</xml>");
        return sb.toString();
    }

    /***
     * 付款成功回调处理，你必须要返回SUCCESS信息给微信服务器，告诉微信服务器我已经收到支付成功的后台通知了。不然的话，微信会一直调用该回调地址，
     * 当达到8次的时候还是没有收到SUCCESS的返回，微信服务器则认为此订单支付失败。
     * 
     * 该回调地址是异步的。 这里可以处理数据库中的订单状态.。 
     * 
     * 作者: zhoubang 日期：2015年6月10日 上午9:25:29
     * 
     * @param request
     * @param response
     * @throws IOException
     * @throws JDOMException
     */
    @SuppressWarnings("unchecked")
    @RequestMapping(value = "pay")
    public @ResponseBody void notify_success(HttpServletRequest request,
            HttpServletResponse response) throws IOException, JDOMException {
        InputStream inStream = request.getInputStream();
        ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = inStream.read(buffer)) != -1) {
            outSteam.write(buffer, 0, len);
        }
        log.info("~~~~~~~~~~~~~~~~付款成功~~~~~~~~~");
        outSteam.close();
        inStream.close();

        /** 支付成功后，微信回调返回的信息 */
        String result = new String(outSteam.toByteArray(), "utf-8");
        Map<Object, Object> map = XMLUtil.doXMLParse(result);
        for (Object keyValue : map.keySet()) {
            /** 输出返回的订单支付信息 */
            log.info(keyValue + "=" + map.get(keyValue));
        }
        if (map.get("result_code").toString().equalsIgnoreCase("SUCCESS")) {
            
            //告诉微信服务器，我收到信息了，不要在调用回调方法(/pay)了
            /*
             * 【需要注意】：
             *      后期很多朋友都反应说，微信会一直请求这个回调地址，也都是使用的是 response.getWriter().write(setXML("SUCCESS", ""));
             *      百思不得其解，最终发现处理办法。其实不能直接使用response.getWriter()返回结果，这样微信是接收不到的。
             *      只能使用OutputStream流的方式返回结果给微信。
             *      切记！！！！
             * */
            OutputStream outputStream = null;
            try {
                outputStream = response.getOutputStream();
                outputStream.flush();
                outputStream.write(setXML("SUCCESS", "").getBytes());
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                try {
                    outputStream.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            
            //response.getWriter().write(setXML("SUCCESS", "")); //不能使用这种方式，不然微信接收不到结果，必须使用OutputStream流的方式返回
            
            log.info("-------------" + setXML("SUCCESS", ""));
        }
    }

    /**
     * 发送xml格式数据到微信服务器 告知微信服务器回调信息已经收到。
     * 
     * 作者: zhoubang 日期：2015年6月10日 上午9:27:33
     * 
     * @param return_code
     * @param return_msg
     * @return
     */
    public static String setXML(String return_code, String return_msg) {
        return "<xml><return_code><![CDATA[" + return_code
                + "]]></return_code><return_msg><![CDATA[" + return_msg
                + "]]></return_msg></xml>";
    }

    /**
     * 支付成功请求的地址URL，告知用户已经支付成功 这个回调是同步回调，只是提供我们自定义的支付成功页面展示给用户看的。不做任何后台订单的处理 
     * 
     * 作者: zhoubang 日期：2015年6月10日 上午10:37:35
     * 
     * @param request
     * @param response
     * @param money
     *            金额单位为分
     * @return
     */
    @RequestMapping("paysuccess")
    public ModelAndView paysuccess(HttpServletRequest request,
            HttpServletResponse response, Integer totalPrice) {
        // 跳转到paysuccess.jsp页面，告诉用户已经成功支付了多少钱
        ModelAndView mav = new ModelAndView("forward:" + "/paysuccess.jsp");
        /** 将分转换为元 */
        BigDecimal b1 = new BigDecimal(totalPrice);
        BigDecimal b2 = new BigDecimal(100);

        mav.addObject("money", b1.divide(b2));
        return mav;
    }

}

